최근 Mocus 작업 중에 북마크 기능 추가중에 세션 상태에 관해 이슈가 발생했습니다.
기존에 Supabase Authentication을 사용해 세션 관리를 하고 있었으나, 초기에는 클라이언트에서 세션 상태를 유지하지 않고 사용자 정보가 필요할 때마다 Supabase에서 인증 정보를 매번 가져오는 구조였습니다.
MVP 개발에 초점을 맞추다 보니 세션 관리 최적화가 미뤄졌었지만, 목업 아이템 리스트에서 문제가 두드러졌습니다. 세션 정보와 북마크 여부를 조회하는 API를 공용으로 사용되는 북마크 컴포넌트 내부에서 사용하고 있었기 때문에 당연하게도 렌더링 시점에 각 아이템마다 별도의 세션 정보를 요청하면서 리스트 아이템 수에 비례하여 API 호출이 증가했습니다.
예를 들어, 10개의 아이템을 렌더링할 때
동일한 세션 정보를 여러 번 요청하는 비효율적인 패턴이 발생했습니다. Next.js의 클라이언트 서버에 대한 내부 요청이라 하더라도 불필요한 API 호출이 발생했습니다.
민감하지 않은 세션 정보를 앱 초기화 시 한 번만 fetch하고 Zustand를 사용해 전역 상태로 SessionStore 관리하도록 변경했습니다.
// session-store.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import supabase from '../supabase/client';
interface Session {
id: string;
email?: string;
name?: string;
avatar_url?: string;
expires?: string;
}
interface SessionState {
session: Session | null;
isLoading: boolean;
error: Error | null;
setSession: (session: Session | null) => void;
clearSession: () => void;
fetchSession: () => Promise<void>;
}
export const useSessionStore = create<SessionState>()(
devtools(
persist(
(set) => ({
session: null,
isLoading: false,
error: null,
setSession: (session) => set({ session }),
clearSession: () => set({ session: null }),
fetchSession: async () => {
try {
set({ isLoading: true, error: null });
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
set({ session: null, isLoading: false });
return;
}
const session = {
id: user?.id,
email: user?.email,
name: user?.user_metadata.name,
avatar_url: user?.user_metadata.avatar_url,
};
set({ session, isLoading: false });
} catch (error) {
set({ error: error as Error, isLoading: false });
console.error('Error fetching session:', error);
}
},
}),
{
name: 'session-storage',
partialize: (state) => ({ session: state.session }),
},
),
),
);
useSessionStore를 통한 전역 관리 상태를 구성
// client-layout.tsx
'use client';
import { useEffect } from 'react';
import { useSessionStore } from './_libs/zustand/session-store';
export default function ClientLayout({ children }: { children: React.ReactNode }) {
const fetchSession = useSessionStore(state => state.fetchSession);
useEffect(() => {
fetchSession();
}, [fetchSession]);
return <>{children}</>;
}
루트 layout에서는 서버 컴포넌트로 사용하기 때문에 클라이언트에서 훅을 사용하기 위한 클라이언트용 레이아웃을 별도로 구성했습니다.
간단한 형태로 API 호출을 크게 줄이고 앱 성능을 향상시켰습니다.